package com.googlecode.restitory.api.gae;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.appengine.api.datastore.Cursor;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.KeyRange;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.datastore.Query.SortDirection;
import com.google.appengine.api.datastore.QueryResultList;
import com.google.appengine.api.datastore.Text;
import com.google.appengine.api.datastore.Transaction;
import com.google.appengine.api.datastore.TransactionOptions;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.googlecode.mycontainer.commons.file.PathUtil;
import com.googlecode.restitory.api.ConflictException;
import com.googlecode.restitory.api.JsonHelper;
import com.googlecode.restitory.api.QueryOperator;
import com.googlecode.restitory.api.QuerySort;
import com.googlecode.restitory.api.QueryType;
import com.googlecode.restitory.gae.filter.util.JsonUtil;

public class DatastoreJsonService implements TxJsonService {

	private static final Logger LOG = LoggerFactory
			.getLogger(DatastoreJsonService.class);

	public static final int STRING_MAX_FIELD_LENGTH = 500;

	public static boolean SUPPORT_TX = true;

	private Transaction transaction;

	private final DatastoreService service;

	public DatastoreJsonService() {
		service = DatastoreServiceFactory.instance().getDatastoreService();
	}

	public void begin() {
		if (SUPPORT_TX) {
			TransactionOptions options = TransactionOptions.Builder
					.withXG(true);
			transaction = getDatastoreService().beginTransaction(options);
		}
	}

	public void commit() {
		if (transaction != null) {
			transaction.commit();
		}
	}

	public void rollback() {
		if (transaction != null) {
			try {
				transaction.rollback();
			} catch (Exception e) {
				throw new RuntimeException("rollback error", e);
			}
		}
	}

	public Transaction getTransaction() {
		return transaction;
	}

	public void setTransaction(Transaction transaction) {
		this.transaction = transaction;
	}

	@Override
	public String put(JsonObject obj) {
		DatastoreService service = getDatastoreService();
		checkConflict(obj);
		Entity entity = jsonToEntity(obj);
		entity.setProperty("_lastModified", System.currentTimeMillis());
		service.put(transaction, entity);
		return keyToString(entity.getKey());
	}

	private Long checkConflict(JsonObject obj) {
		Number rev = (Number) JsonHelper.jsonToBasicObject(obj.get("_rev"));
		if (rev == null) {
			obj.addProperty("_rev", 1l);
			return 1l;
		}
		try {
			DatastoreService service = getDatastoreService();
			Key key = extractKey(obj);
			Entity old = service.get(transaction, key);
			Long oldRev = (Long) old.getProperty("_rev");
			if (oldRev == null) {
				obj.addProperty("_rev", 1l);
				return 1l;
			}
			if (rev.longValue() < 0) {
				obj.remove("_rev");
				entityToJson(old, obj);
				rev = obj.get("_rev").getAsNumber();
			}
			if (oldRev.longValue() != rev.longValue()) {
				throw new ConflictException();
			}
			long nrev = oldRev.longValue() + 1;
			obj.addProperty("_rev", nrev);
			return nrev;
		} catch (EntityNotFoundException e) {
			if (rev.longValue() != 0l) {
				throw new ConflictException();
			}
			obj.addProperty("_rev", 1l);
			return 1l;
		}
	}

	public static String getParent(Key key) {
		String k = key.getKind();
		if (!k.startsWith("r-")) {
			throw new RuntimeException("it is not a resource: " + k);
		}
		if (k.equals("r-root")) {
			return null;
		}
		return k.substring(2);
	}

	public static String keyToString(Key key) {
		return PathUtil.concatPath(getParent(key), key.getName());
	}

	public static JsonArray keyToString(Iterable<Key> keys) {
		JsonArray ret = new JsonArray();
		for (Key key : keys) {
			ret.add(new JsonPrimitive(keyToString(key)));
		}
		return ret;
	}

	public static Entity jsonToEntity(JsonObject obj) {
		Key key = extractKey(obj);
		Entity ret = new Entity(key);
		return jsonToEntity(obj, ret, null);
	}

	public static Entity jsonToEntity(JsonObject obj, Entity ret, JsonDesc desc) {
		Set<Entry<String, JsonElement>> set = obj.entrySet();
		for (Entry<String, JsonElement> entry : set) {
			String name = entry.getKey();
			JsonElement value = entry.getValue();
			if (name.equals("_self")) {
				continue;
			}
			try {
				Object v = JsonHelper.jsonToBasicObject(value);
				if (v instanceof String) {
					String s = (String) v;
					if (s.length() > STRING_MAX_FIELD_LENGTH) {
						ret.setUnindexedProperty(name,
								new Text(value.toString()));
						continue;
					}
				}
				if (desc == null) {
					ret.setProperty(name, v);
					continue;
				}
				JsonPropertyDesc propDesc = desc.get(name);
				if (propDesc == null) {
					throw new RuntimeException("propertyDesc not found: '"
							+ name + "' " + desc + " [" + obj + "]");
				}
				if (!propDesc.isCan()) {
					throw new RuntimeException("property denied: '" + name
							+ "' " + propDesc + " [" + obj + "]");
				}
				if (propDesc.isIndexed()) {
					ret.setProperty(name, v);
				} else {
					ret.setUnindexedProperty(name, v);
				}
			} catch (JsonParseException e) {
				String v = value.toString();
				ret.setUnindexedProperty(name, new Text(v));
			}
		}
		return ret;
	}

	public static Key extractKey(JsonObject obj) {
		return stringToKey(obj.get("_self").getAsJsonPrimitive().getAsString());
	}

	public static Key stringToKey(String path) {
		path = PathUtil.fix(path);
		String parent = PathUtil.parentPath(path);
		String kind = createKind(parent);
		String name = "/".equals(path) ? "root" : PathUtil.getName(path);
		return KeyFactory.createKey(kind, name);
	}

	public static String createKind(String parent) {
		parent = PathUtil.fix(parent);
		if (parent == null) {
			parent = "r-root";
		}
		return "r-" + parent;
	}

	public DatastoreService getDatastoreService() {
		return service;
	}

	@Override
	public String post(JsonObject obj) {
		if (JsonHelper.jsonToBasicObject(obj.get("_rev")) != null) {
			throw new ConflictException();
		}
		String kind = obj.getAsJsonPrimitive("_self").getAsString();
		String path = generatePath(kind);
		obj.addProperty("_self", path);
		return put(obj);
	}

	@Override
	public List<String> generatePaths(String name, int size) {
		if (name == null || size == 0) {
			return Collections.emptyList();
		}
		ArrayList<String> ret = new ArrayList<String>(size);
		DatastoreService s = getDatastoreService();
		KeyRange range = s.allocateIds("sequence", size);
		for (Key key : range) {
			String path = PathUtil.concatPath(name, Long.toString(key.getId()));
			ret.add(path);
		}
		return ret;
	}

	@Override
	public String generatePath(String name) {
		if (name == null) {
			return null;
		}
		return generatePaths(name, 1).get(0);
	}

	@Override
	public JsonObject get(String id) {
		try {
			DatastoreService service = getDatastoreService();
			Key key = stringToKey(id);
			Entity entity = service.get(transaction, key);
			JsonObject ret = entityToJson(entity);
			return ret;
		} catch (EntityNotFoundException e) {
			return null;
		}
	}

	public static JsonObject entityToJson(Entity entity) {
		return entityToJson(entity, new JsonObject());
	}

	public static JsonObject entityToJson(Entity entity, JsonObject ret) {
		ret.addProperty("_self", keyToString(entity.getKey()));
		Set<Entry<String, Object>> set = entity.getProperties().entrySet();
		for (Entry<String, Object> entry : set) {
			String name = entry.getKey();
			Object value = entry.getValue();
			if (!ret.has(name)) {
				ret.add(name, entityToJsonAttribute(value));
			}
		}
		return ret;
	}

	private static JsonElement entityToJsonAttribute(Object value) {
		if (value instanceof Text) {
			return JsonHelper.parse(((Text) value).getValue());
		} else {
			return JsonHelper.createBasic(value);
		}
	}

	@Override
	public void delete(String id) {
		delete(id, null);
	}

	@Override
	public void delete(String id, Long rev) {
		if (rev != null) {
			JsonObject obj = new JsonObject();
			obj.addProperty("_self", id);
			obj.addProperty("_rev", rev);
			checkConflict(obj);
		}
		Key key = stringToKey(id);
		DatastoreService service = getDatastoreService();
		service.delete(transaction, key);
	}

	public JsonObject cursor(JsonObject query) {
		if (!query.has("cursor") || query.get("cursor").isJsonNull()) {
			query.addProperty("cursor", true);
		}
		JsonArray result = query(query);
		query.remove("o");
		JsonObject ret = new JsonObject();
		ret.add("result", result);
		ret.add("query", query);
		return ret;
	}

	@Override
	public JsonArray query(JsonObject query) {
		Query gq = createQuery(query);
		QueryType type = JsonHelper.jsonToEnum(QueryType.class,
				query.get("type"), QueryType.PATH);
		if (type.equals(QueryType.PATH) || type.equals(QueryType.DELETE)) {
			gq.setKeysOnly();
		}
		DatastoreService service = getDatastoreService();
		if (LOG.isDebugEnabled()) {
			LOG.debug("Query: " + gq);
		}
		PreparedQuery prepared = service.prepare(gq);
		FetchOptions opts = createFetchOptions(query);
		QueryResultList<Entity> iterable = prepared.asQueryResultList(opts);

		if (type.equals(QueryType.DELETE)) {
			JsonArray entities = entityToPath(iterable);
			delete(iterable);
			return entities;
		}

		if (type.equals(QueryType.PATH)) {
			JsonArray ret = entityToPath(iterable);
			setCursor(iterable, query);
			return ret;
		}
		String attribute = (String) JsonHelper.jsonToBasicObject(query
				.get("attribute"));
		if (attribute != null) {
			JsonArray ret = entityToJson(iterable, attribute);
			setCursor(iterable, query);
			return ret;
		}
		JsonArray ret = entityToJson(iterable);
		setCursor(iterable, query);
		return ret;
	}

	private void setCursor(QueryResultList<Entity> iterable, JsonObject query) {
		if (query.has("cursor") && !query.get("cursor").isJsonNull()) {
			query.addProperty("cursor", iterable.getCursor().toWebSafeString());
		}
	}

	private void delete(Iterable<Entity> iterable) {
		Iterable<Key> keys = entityToKeys(iterable);
		getDatastoreService().delete(transaction, keys);
	}

	private Iterable<Key> entityToKeys(Iterable<Entity> iterable) {
		ArrayList<Key> ret = new ArrayList<Key>();
		for (Entity entity : iterable) {
			ret.add(entity.getKey());
		}
		return ret;
	}

	private JsonArray entityToJson(Iterable<Entity> iterable, String attribute) {
		JsonArray ret = new JsonArray();
		for (Entity entity : iterable) {
			Object value = entity.getProperty(attribute);
			ret.add(entityToJsonAttribute(value));
		}
		return ret;
	}

	private JsonArray entityToJson(Iterable<Entity> iterable) {
		JsonArray ret = new JsonArray();
		for (Entity entity : iterable) {
			JsonObject json = entityToJson(entity);
			ret.add(json);
		}
		return ret;
	}

	private JsonArray entityToPath(Iterable<Entity> iterable) {
		JsonArray ret = new JsonArray();
		for (Entity entity : iterable) {
			String path = keyToString(entity.getKey());
			ret.add(new JsonPrimitive(path));
		}
		return ret;
	}

	private FetchOptions createFetchOptions(JsonObject query) {
		FetchOptions ret = FetchOptions.Builder.withDefaults();
		Number o = (Number) JsonHelper.jsonToBasicObject(query.get("o"));
		Number l = (Number) JsonHelper.jsonToBasicObject(query.get("l"));
		Object cursor = JsonHelper.jsonToBasicObject(query.get("cursor"));
		if (cursor != null && cursor instanceof String) {
			ret.startCursor(Cursor.fromWebSafeString((String) cursor));
		}
		if (o != null) {
			ret.offset(o.intValue());
		}
		if (l != null && l.intValue() >= 0) {
			ret.limit(l.intValue());
		}
		return ret;
	}

	private Query createQuery(JsonObject query) {
		String kind = (String) JsonHelper.jsonToBasicObject(query.get("kind"));
		kind = createKind(kind);
		Query ret = kind == null ? new Query() : new Query(kind);
		queryFilter(query, ret);
		querySort(query, ret);
		return ret;
	}

	@SuppressWarnings("unchecked")
	private void queryFilter(JsonObject query, Query ret) {
		JsonObject filter = query.getAsJsonObject("filter");
		if (filter == null) {
			return;
		}
		Set<Entry<String, JsonElement>> set = filter.entrySet();
		for (Entry<String, JsonElement> entry : set) {
			String fieldName = entry.getKey();
			Set<Entry<String, JsonElement>> comps = entry.getValue()
					.getAsJsonObject().entrySet();
			for (Entry<String, JsonElement> comp : comps) {
				QueryOperator op = QueryOperator.valueOf(comp.getKey());
				Object value = JsonHelper.jsonToBasicObject(comp.getValue());
				if (!QueryOperator.IN.equals(op) && value instanceof List) {
					List<Object> array = (List<Object>) value;
					for (Object o : array) {
						ret.addFilter(fieldName, operatorToGAE(op), o);
					}
				} else {
					ret.addFilter(fieldName, operatorToGAE(op), value);
				}
			}
		}
	}

	private FilterOperator operatorToGAE(QueryOperator op) {
		switch (op) {
		case EQ:
			return FilterOperator.EQUAL;
		case GT:
			return FilterOperator.GREATER_THAN;
		case GE:
			return FilterOperator.GREATER_THAN_OR_EQUAL;
		case LT:
			return FilterOperator.LESS_THAN;
		case LE:
			return FilterOperator.LESS_THAN_OR_EQUAL;
		case IN:
			return FilterOperator.IN;

		}
		throw new RuntimeException("not supported: " + op);
	}

	private void querySort(JsonObject query, Query ret) {
		JsonArray array = query.getAsJsonArray("sort");
		if (array != null) {
			for (JsonElement s : array) {
				if (s.isJsonPrimitive()) {
					ret.addSort(s.getAsJsonPrimitive().getAsString());
				} else {
					Set<Entry<String, JsonElement>> set = s.getAsJsonObject()
							.entrySet();
					for (Entry<String, JsonElement> entry : set) {
						QuerySort t = JsonHelper.jsonToEnum(QuerySort.class,
								entry.getValue(), QuerySort.ASC);
						ret.addSort(entry.getKey(), querySortToDirection(t));
					}
				}
			}
		}
	}

	private SortDirection querySortToDirection(QuerySort t) {
		if (t == null) {
			return null;
		}
		switch (t) {
		case ASC:
			return SortDirection.ASCENDING;
		case DESC:
			return SortDirection.DESCENDING;
		}
		throw new RuntimeException("unknown type: " + t);
	}

	@Override
	public Long count(JsonObject query) {
		Query gq = createQuery(query);
		QueryType type = JsonHelper.jsonToEnum(QueryType.class,
				query.get("type"), QueryType.PATH);
		if (type.equals(QueryType.PATH)) {
			gq.setKeysOnly();
		}
		DatastoreService service = getDatastoreService();
		if (LOG.isDebugEnabled()) {
			LOG.debug("Query: " + gq);
		}
		PreparedQuery prepared = service.prepare(gq);
		FetchOptions opts = createFetchOptions(query);
		return new Long(prepared.countEntities(opts));
	}

	public boolean isActive() {
		if (transaction == null) {
			return false;
		}
		return transaction.isActive();
	}

	@Override
	public JsonArray getAll(Iterable<String> ids) {
		DatastoreService service = getDatastoreService();
		Iterable<Key> keys = stringToKey(ids);
		Map<Key, Entity> entities = service.get(transaction, keys);
		JsonArray ret = new JsonArray();
		for (Key key : keys) {
			Entity entity = entities.get(key);
			if (entity != null) {
				ret.add(entityToJson(entity));
			}
		}
		return ret;
	}

	public static ArrayList<Key> stringToKey(Iterable<String> ids) {
		ArrayList<Key> ret = new ArrayList<Key>();
		for (String id : ids) {
			ret.add(stringToKey(id));
		}
		return ret;
	}

	@Override
	public JsonArray putAll(Iterable<JsonObject> jsons) {
		DatastoreService service = getDatastoreService();

		ArrayList<Entity> entities = new ArrayList<Entity>();
		for (JsonObject json : jsons) {
			if (json.has("_rev") && json.get("_rev").isJsonNull()) {
				throw new RuntimeException(
						"batch with _rev is not supported yet: " + json);
			}
			json.addProperty("_rev", 1l);
			Entity entity = jsonToEntity(json);
			entity.setProperty("_lastModified", System.currentTimeMillis());
			entities.add(entity);
		}

		List<Key> keys = service.put(transaction, entities);
		JsonArray ret = keyToString(keys);
		return ret;
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	@Override
	public JsonArray putAll(JsonArray jsons) {
		return putAll((Iterable) jsons);
	}

	@Override
	public JsonArray postAll(Iterable<JsonObject> jsons) {
		Map<String, List<JsonObject>> kinds = new HashMap<String, List<JsonObject>>();
		for (JsonObject obj : jsons) {
			if (JsonHelper.jsonToBasicObject(obj.get("_rev")) != null) {
				throw new ConflictException();
			}
			String kind = obj.getAsJsonPrimitive("_self").getAsString();
			List<JsonObject> list = kinds.get(kind);
			if (list == null) {
				list = new ArrayList<JsonObject>();
				kinds.put(kind, list);
			}
			list.add(obj);
		}
		Set<Entry<String, List<JsonObject>>> set = kinds.entrySet();
		for (Entry<String, List<JsonObject>> entry : set) {
			String kind = entry.getKey();
			List<JsonObject> list = entry.getValue();
			Iterator<String> it = generatePaths(kind, list.size()).iterator();
			for (JsonObject json : list) {
				json.addProperty("_self", it.next());
			}
		}
		return putAll(jsons);

	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	@Override
	public JsonArray postAll(JsonArray jsons) {
		return postAll((Iterable) jsons);
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	@Override
	public JsonArray getAll(JsonArray paths) {
		return getAll((Iterable) JsonUtil.value(paths));
	}

	@Override
	public void deleteAll(Iterable<String> paths) {
		DatastoreService service = getDatastoreService();
		Iterable<Key> keys = stringToKey(paths);
		service.delete(transaction, keys);
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	@Override
	public void deleteAll(JsonArray paths) {
		deleteAll((Iterable) JsonUtil.value(paths));
	}
}
